# coding:utf-8
"""
a simple 2048 game.

author: Macussair

"""
import random
import functools
from collections import deque
import tkinter
import tkinter.messagebox as msgbox
from enum import Enum

# You win the game when GOAL is achieved
# 达到GOAL后完成游戏
GOAL = 2048
SCORE_FILE = 'high_score.dat'

class Game2048:
    # game logic
    GameState = Enum('GameState', ('running', 'game_over', 'winning', 'post_game'))
    def __init__(self, w=4, h=4):
        self.board = [[0] * w for i in range(h)]
        self.w = w
        self.h = h
        self.load_score()

    def load_score(self):
        # load high score from file (not yet)
        try:
            with open(SCORE_FILE, 'r') as scorefile:
                line = scorefile.readline()
                self.highscore = int(line)
        except:
            self.highscore = 100


    def update_highscore(self):
        if self.score> self.highscore:
            self.highscore = self.score
            if self.status == self.GameState.game_over:
                with open(SCORE_FILE, 'w') as scorefile:
                    scorefile.write(str(self.highscore))

    def move_check(func):
        # a decrator does all the dirty jobs like check if it's game over and update the highscore
        def _move_check(*args, **kwargs):
            func(*args, **kwargs)
            self = args[0]
            self.check_game()
            self.update_highscore()
        return _move_check

    def start(self):
        # start or restart the game
        self.board = [[0] * self.w for i in range(self.h)]
        self.score = 0
        self.status = self.GameState.running
        random.seed()
        for i in range(3):
            self.rand_put(2)

    def rand_put(self, num):
        # put num at random space
        while True:
            col, row = random.randrange(self.w), random.randrange(self.h)
            if self.board[row][col] == 0:
                self.board[row][col] = num
                break

    def _try_left(self, board):
        # helper function try move left, return True if it really moved
        moved = False
        que = deque()
        for row in range(self.h):
            non_zeros = []
            for num in board[row]:
                if num != 0:
                    non_zeros.append(num)
            if not non_zeros:# empty row
                continue
            # more than one number
            que.clear()
            i, j = 0, 1
            while True:
                if i >= len(non_zeros):# no more num
                    break
                num = non_zeros[i]
                if j >= len(non_zeros):# last num
                    que.append(num)
                    break
                num_next = non_zeros[j]
                if num != num_next:
                    que.append(num)
                    i += 1
                    j += 1
                else:
                    # same value, merge
                    new_num = num * 2
                    que.append(new_num)
                    self.score += new_num
                    i += 2
                    j += 2
                    if new_num == GOAL:
                        self.status = self.GameState.winning
            empty_space = self.w - len(que)
            que.extend([0] * empty_space)
            alist = list(que)
            if alist!=board[row]:
                moved = True
                board[row] = alist
        return moved

    def _try_right(self, board):
        # similar to _try_left but move right this time
        moved = False
        que = deque()
        for row in range(self.h):
            non_zeros = []
            for num in reversed(board[row]):
                if num != 0:
                    non_zeros.append(num)
            if not non_zeros:# empty row
                continue
            # more than one number
            que.clear()
            i, j = 0, 1
            while True:
                if i >= len(non_zeros):# no more num
                    break
                num = non_zeros[i]
                if j >= len(non_zeros):# last num
                    que.appendleft(num)
                    break
                num_next = non_zeros[j]
                if num != num_next:
                    que.appendleft(num)
                    i += 1
                    j += 1
                else:
                    # same value, merge
                    new_num = num * 2
                    que.appendleft(new_num)
                    self.score += new_num
                    i += 2
                    j += 2
                    if new_num == GOAL:
                        self.status = self.GameState.winning
            empty_space = self.w - len(que)
            que.extendleft([0] * empty_space)
            alist = list(que)
            if alist!=board[row]:
                moved = True
                board[row] = alist
        return moved

    @move_check
    def move_left(self):
        if self._try_left(self.board):
            self.rand_put(2)

    @move_check
    def move_right(self):
        if self._try_right(self.board):
            self.rand_put(2)

    def check_game(self):
        if self.status == self.GameState.running or self.status == self.GameState.post_game:
            if self.is_game_over():
                self.status = self.GameState.game_over
    def is_game_over(self):
        for row1, row2 in zip(self.board, self.board[1:]):
            for up, down in zip(row1, row2):
                if up == 0 or down == 0 or up == down:
                # empty space or movable
                    return False
            for left,right in zip(row1, row1[1:]):
                if left == right:
                    return False
        # last row
        row = self.board[-1]
        for left,right in zip(row, row[1:]):
            if left == right:
                return False
        return True

    @staticmethod
    def _transpose(a2d):
        transposed = [[] for x in a2d[0]]
        for row in a2d:
            for i,num in enumerate(row):
                transposed[i].append(num)
        return transposed

    @move_check
    def move_up(self):
        # reuse the _try_left by transposing the board
        transposed = self._transpose(self.board)
        if self._try_left(transposed):
            self.board = self._transpose(transposed)
            self.rand_put(2)

    @move_check
    def move_down(self):
        transposed = self._transpose(self.board)
        if self._try_right(transposed):
            self.board = self._transpose(transposed)
            self.rand_put(2)

    def __str__(self):
        return '====board====\n' + functools.reduce(lambda x,y: str(x)+'\n'+str(y), self.board)

    __repr__ = __str__

class display_behavier:
    def display(self):
        pass

class display_tk(tkinter.Frame, display_behavier):
    def __init__(self, game, master = None):
        super().__init__(master)
        self.pack()
        self.game = game
        self.add_status()
        self.add_board()
        self.add_menu()
        # self.add_controls()
        self.key_binding = {'Left':self.move_left, 'Right':self.move_right, 'Up':self.move_up, 'Down':self.move_down}
        self.bind('<Key>', self.key_manage)
        self.focus_set()

    def add_menu(self):
        menu = tkinter.Menu(self)
        self.master['menu'] = menu
        menu_game = tkinter.Menu(menu)
        menu.add_cascade(menu = menu_game, label='游戏')
        menu_game.add_command(label='重新开始', command=self.restart)
        menu_game.add_command(label='结束', command=self.master.destroy)
        menu_help = tkinter.Menu(menu)
        menu.add_cascade(menu = menu_help, label='帮助')
        menu_help.add_command(label='关于', command=self.about)

    def add_status(self):
        status_bar = tkinter.Frame(self)
        status_bar.pack()
        logo = tkinter.Frame(status_bar)
        logo.pack(side='left')
        self.logo_button = tkinter.Button(logo,width=5, height=2, text='2048', fg = '#FFF', bg = '#AA0')
        self.logo_button.pack(side='left')
        self.logo_button['command'] = self.restart
        right_side = tkinter.Frame(status_bar)
        right_side.pack(side='right')
        labels = tkinter.Frame(right_side)
        labels.pack(side='left')
        values = tkinter.Frame(right_side)
        values.pack(side='right')
        self.score = tkinter.Label(values, text = '0')
        self.score.pack(side='top')
        tkinter.Label(labels, text='分数').pack(side='top')
        self.highscore = tkinter.Label(values, text = '0')
        self.highscore.pack(side='top')
        tkinter.Label(labels, text='最高分').pack(side='top')
    def add_board(self):
        self.rows = [tkinter.Frame(self) for i in range(self.game.h)]
        self.buttons = []
        for p in self.rows:
            p.pack()
            buttons_in_a_row = [tkinter.Button(p, width=5, height=2) for i in range(self.game.w)]
            self.buttons.append(buttons_in_a_row)
            for button in buttons_in_a_row:
                button.pack(side='left')
    def add_controls(self):
        controls = tkinter.Frame(self)
        controls.pack(side='top')
        tkinter.Button(controls, text='Left', command=self.move_left).pack(side='left')
        tkinter.Button(controls, text='Right', command=self.move_right).pack(side='right')
        tkinter.Button(controls, text='Up', command=self.move_up).pack(side='top')
        tkinter.Button(controls, text='Down', command = self.move_down).pack()

    def key_manage(self, event):
        # response to the keyboard event
        key = event.keysym
        # print(key)
        if key in self.key_binding.keys():
            self.key_binding[key]()
        self.display()
        self.check_game_over()
        self.focus_set()

    def restart(self):
        print('Game restarted.')
        self.game.start()
        self.display()

    def check_game_over(self):
        if self.game.status == Game2048.GameState.game_over:
            isretry = msgbox.askretrycancel('Game Over', 'Game is Over.')
            if isretry == True:
                self.restart()
            else:
                print('Lock the Game.')
        elif self.game.status == Game2048.GameState.winning:
            print('You win.')
            self.game.status = Game2048.GameState.post_game

    def move_left(self):
        self.game.move_left()
    def move_right(self):
        self.game.move_right()
    def move_up(self):
        self.game.move_up()
    def move_down(self):
        self.game.move_down()

    def display(self):
        self.score['text'] = str(self.game.score)
        self.highscore['text'] = str(self.game.highscore)
        for row_game, row_button in zip(self.game.board, self.buttons):
            for grid_game, grid_button in zip(row_game, row_button):
                grid_button['text'] = str(grid_game) if grid_game != 0 else '  '
                color = {0:'#FFF', 2:'#F00', 4:'#F10', 8:'#F20', 16:'#F30',
                         32:'#F40', 64:'#F50', 128:'#F60', 256:'#F70',
                         512:'#F80', 1024:'#F90', 2048:'#FA0'}
                grid_button['width'] = 5
                if grid_game in color.keys():
                    grid_button['bg'] = color[grid_game]
                else:
                    grid_game['bg'] = color['2048']

    def about(self):
        msgbox.showinfo(title='关于', message='版本:0.1.0 \n作者：Macussair')

if __name__ == '__main__':
    g = Game2048()
    g.start()
    root = tkinter.Tk()
    app = display_tk(g)
    app.display()
    root.mainloop()
